package mobi.smarthosts;

import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.text.DecimalFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import android.content.Context;
import android.os.Build;

public final class Utils {

    public static final long SECOND_MILLIS = 1000;
    public static final long MINUTE_MILLIS = SECOND_MILLIS * 60;
    public static final long HOUR_MILLIS = MINUTE_MILLIS * 60;
    public static final long DAY_MILLIS = HOUR_MILLIS * 24;
    public static final long WEEK_MILLIS = DAY_MILLIS * 7;
    public static final long MONTH_MILLIS = DAY_MILLIS * 30;
    public static final long YEAR_MILLIS = DAY_MILLIS * 365;

    public static final int TIME_OFFSET_FROM_2001 = 978307200;

    public static final String TIMEZONE_PATTERN = "^([\\+|-]?)(\\d?\\d)(\\d\\d)";
    public static final String EMAIL_PATTERN = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,4}$";

    public final static String DEFAULT_DATE_FORMAT = "MM/dd/yy";
    public final static String DEFAULT_TIME_FORMAT = "HH:mm";
    public final static String DEFAULT_DATETIME_FORMAT = "MM/dd/yy HH:mm";

    private Utils() {}

    //********************* Empty Object Validation ***************************

    public static boolean isZero(Long longNum) {
        return longNum == null || longNum == 0;
    }

    public static boolean isZero(Integer integer) {
        return integer == null || integer == 0;
    }

    public static boolean isZero(Double doubleNum) {
        return doubleNum == null || doubleNum == 0;
    }

    public static boolean isEmptyString(String string) {
        return string == null || string.length() <= 0 || "null".equals(string);
    }

    public static boolean isEmptyArray(Object[] array) {
        return array == null || array.length == 0;
    }

    public static boolean isExistedFile(File file) {
        return file != null && file.exists();
    }

    public static boolean isEmptyFile(File file) {
        return file == null || !file.exists() || file.length() <= 0;
    }

    @SuppressWarnings("rawtypes")
    public static boolean isEmptyCollection(Collection collection) {
        return collection == null || collection.isEmpty();
    }

    @SuppressWarnings("rawtypes")
    public static boolean isEmptyMap(Map map) {
        return map == null || map.isEmpty();
    }

    public static boolean isEmptyDate(Date date) {
        return date == null || date.getTime() == 0;
    }

    //****************************** String ***********************************

    public static String trimLastChar(String str) {
        return str.substring(0, str.length()-1);
    }

    public static String trimSpace(String str) {
        return str.replace(" ", "");
    }

    public static String convertStringArray(String[] strArray) {
        String result = "";
        for (String str : strArray) {
            if (!isEmptyString(str)) result += str+",";
        }
        return trimLastChar(result);
    }

    public static String convertStringArrayWithQuota(String[] strArray) {
        String result = "";
        for (String str : strArray) {
            if (!isEmptyString(str)) result += "'"+str+"',";
        }
        return trimLastChar(result);
    }

    public static boolean isValidEmailAddress(String str) {
        return !isEmptyString(str) && str.matches(EMAIL_PATTERN);
    }

    //****************************** Collection *******************************

    public static <T> Set<T> convertListToSet(List<T> list) {
        Set<T> set = new HashSet<T>();
        set.addAll(list);
        return set;
    }

    public static <T> List<T> convertSetToList(Set<T> set) {
        List<T> list = new ArrayList<T>();
        list.addAll(set);
        return list;
    }

    //******************************** Date ***********************************

    public static boolean isSameDate(Date date1, Date date2) {
        return date1.getYear() == date2.getYear()
            && date1.getMonth() == date2.getMonth()
            && date1.getDate() == date2.getDate();
    }

    public static String formatDate(Date date) {
        return new SimpleDateFormat(DEFAULT_DATE_FORMAT).format(date);
    }

    public static String formatDateTime(Date date) {
        return new SimpleDateFormat(DEFAULT_DATETIME_FORMAT).format(date);
    }

    public static String formatTime(Date date) {
        return new SimpleDateFormat(DEFAULT_TIME_FORMAT).format(date);
    }

    public static String formatDate(Date date, String format) {
        return new SimpleDateFormat(format).format(date);
    }

    public static Long getTimeWithoutMillisecond(Date date) {
        return getTimeWithoutMillisecond(date.getTime());
    }

    public static Long getTimeWithoutMillisecond(Long date) {
        String timeStr = Long.toString(date);
        return (timeStr.length() >= 3)
            ? Long.parseLong(timeStr.substring(0, timeStr.length()-3)) : 0;
    }

    public static Date parseDate(String string, String format) {
        try {
            return new SimpleDateFormat(format).parse(string);
        } catch (ParseException e) {
            throw new SmartHostsException(e);
        }
    }

    public static Date shiftDate(Date date, int days) {
        date.setDate(date.getDate() + days);
        return date;
    }

    public static int getDateInterval(Date date1, Date date2) {
        return Math.round((date1.getTime()-date2.getTime())/DAY_MILLIS);
    }

    //***************************** Number ************************************

    public static String formatDouble(Double number, int demicalBits) {
        StringBuilder sb = new StringBuilder("#.");
        for (int i=1; i<=demicalBits; i++) sb.append("#");
        return new DecimalFormat(sb.toString()).format(number);
    }

    public static String formatDouble(Double number) {
        return new DecimalFormat("#.######").format(number);
    }

    public static String formatInteger(int number, int bits) {
        StringBuilder sb = new StringBuilder();
        for (int i=0; i < bits; i++) sb.append("0");
        return new DecimalFormat(sb.toString()).format(number);
    }

    //**************************** Timezone ***********************************

    /**
     * Formet raw timezone millisecond to readable format
     */
    public static String formatMillisecond(int millisecond) {
        int tempMillisecond = Math.abs(millisecond);
        DecimalFormat formatter = new DecimalFormat("00");
        String hour = formatter.format(tempMillisecond / HOUR_MILLIS);
        String minute = formatter.format((tempMillisecond % HOUR_MILLIS)/MINUTE_MILLIS);
        return new StringBuilder().append(((millisecond >= 0) ? "+" : "-"))
                                  .append(hour)
                                  .append(minute)
                                  .toString();
    }

    /**
     * Pasre defined format +/-MMSS to raw millisecond that could be
     * used to construct timezone object.
     */
    public static int parseMillisecond(String timeString) {
        Pattern pattern = Pattern.compile(TIMEZONE_PATTERN);
        Matcher matcher = pattern.matcher(timeString);
        if (!matcher.matches())
            throw new IllegalArgumentException("Time string format incorrect. Should be in +/-MMSS");
        int hour = Integer.parseInt(matcher.group(2));
        int minute = Integer.parseInt(matcher.group(3));
        boolean minus = matcher.group(1).equals("-");
        return (int) (hour*HOUR_MILLIS + minute*MINUTE_MILLIS) * (minus ? -1 : 1);
    }

    //******************************* Files ***********************************

    public static List<File> listFiles(File dir) {
        if (!dir.isDirectory()) return null;

        List<File> files = new ArrayList<File>();
        for (File file : dir.listFiles()) {
            if (!file.isDirectory()) {
                files.add(file);
            } else {
                List<File> subFiles = listFiles(file);
                files.addAll(subFiles);
            }
        }
        return files;
    }

    public static byte[] fileToBytes(File file) {
        if (isEmptyFile(file)) return null;
        InputStream is = null;
        try {
            is = new FileInputStream(file);
            long length = file.length();
            if (length > Integer.MAX_VALUE) throw new SmartHostsException("File is too large");

            // Create the byte array to hold the data
            byte[] bytes = new byte[(int)length];

            // Read in the bytes
            int offset = 0;
            int numRead = 0;
            while (offset < bytes.length
                   && (numRead=is.read(bytes, offset, bytes.length-offset)) >= 0)
                offset += numRead;

            // Ensure all the bytes have been read in
            if (offset < bytes.length)
                Log.w("Could not completely read file "+file);
            return bytes;
        } catch (IOException e) {
            Log.e("Failed to read file "+file, e);
            return null;
        } finally {
            closeStream(is);
        }
    }

    public static String fileToString(File file) {
        if (isEmptyFile(file)) return null;
        return new String(fileToBytes(file));
    }

    public static File bytesToFile(byte[] bytes, File file) {
        BufferedOutputStream bos = null;
        FileOutputStream fos = null;
        try {
            if (!file.exists()) {
                if (file.getParentFile() != null && !file.getParentFile().exists())
                    file.getParentFile().mkdirs();
                file.createNewFile();
            }
            fos = new FileOutputStream(file);
            bos = new BufferedOutputStream(fos);
            bos.write(bytes);
            bos.flush();
        } catch (FileNotFoundException e) {
            throw new SmartHostsException(e);
        } catch (IOException e) {
            Log.e("Failed to save file "+file, e);
            return null;
        } finally {
            closeStream(fos);
            closeStream(bos);
        }
        return file;
    }

    public static File stringToFile(String string, File file) {
        Writer out = null;
        try {
            if (!file.exists()) {
                if (file.getParentFile() != null && !file.getParentFile().exists())
                    file.getParentFile().mkdirs();
                file.createNewFile();
            }
            out = new OutputStreamWriter(new FileOutputStream(file));
            out.write(string);
        } catch (FileNotFoundException e) {
            throw new SmartHostsException(e);
        } catch (IOException e) {
            Log.e("Failed to save file "+file, e);
            return null;
        } finally {
            closeStream(out);
        }
        return file;
    }

    public static void copyFile(File src, File dst) {
        if (!isExistedFile(src)) return;
        FileInputStream is = null;
        FileOutputStream os = null;
        try{
            is = new FileInputStream(src);
            os = new FileOutputStream(dst);

            byte[] buf = new byte[1024];
            int len;
            while ((len = is.read(buf)) > 0){
                os.write(buf, 0, len);
            }
            os.flush();
        } catch(FileNotFoundException e){
            throw new SmartHostsException(e);
        } catch(IOException e){
            Log.e("Failed to copy file from "+src+" to "+dst, e);
        } finally {
            closeStream(is);
            closeStream(os);
        }
    }

    public static void moveFile(File src, File dst) {
        copyFile(src, dst);
        if (!isEmptyFile(src)) src.delete();
    }

    public static void deleteFolder(File dir) {
        deleteFolder(dir, null);
    }

    public static void deleteFolder(File dir, String skipFilePattern) {
        if (!isExistedFile(dir)) return;
        if (!dir.isDirectory()) throw new IllegalArgumentException();

        for (File fileToBeDeleted : dir.listFiles()) {
            if (fileToBeDeleted.isDirectory()) deleteFolder(fileToBeDeleted, skipFilePattern);
            if (!isEmptyString(skipFilePattern)
                    && fileToBeDeleted.getName().matches(skipFilePattern)) continue;
            fileToBeDeleted.delete();
        }
    }

    public static String getFileNameWithoutExt(File file) {
        if (file == null) return null;
        String fileName = file.getName();
        if (fileName.lastIndexOf('.') < 0) return fileName;
        return fileName.substring(0, fileName.lastIndexOf('.'));
    }

    public static String getFileExtName(File file) {
        if (file == null) return null;
        String fileName = file.getName();
        if (fileName.lastIndexOf('.') < 0) return fileName;
        return fileName.substring(fileName.lastIndexOf('.')+1, fileName.length());
    }

    public static File addSuffixToFileName(File file, String suffix) {
        if (file == null) return null;
        return new File(file.getParent(),
                getFileNameWithoutExt(file)+suffix+"."+Utils.getFileExtName(file));
    }

    //********************** Temp File on data folder *************************

    private final static String TEMP_FILE_NAME = "temp";

    public static File bytesToTempFile(Context context, byte[] bytes) {
        FileOutputStream fos = null;
        try {
            fos = context.openFileOutput(TEMP_FILE_NAME, Context.MODE_WORLD_READABLE);
        } catch (FileNotFoundException e) {
            Log.w("Permission error. Fix permissiong and retry...", e);
            try {
                File file = getTempFile(context);
                if (!file.exists()) {
                    if (file.getParentFile() != null && !file.getParentFile().exists())
                        file.getParentFile().mkdirs();
                    file.createNewFile();
                }
                ShellExecutor.executeRootCommand("chmod 666 "+getTempFile(context).getAbsolutePath());
                fos = context.openFileOutput(TEMP_FILE_NAME, Context.MODE_WORLD_READABLE);
            } catch (FileNotFoundException ex) {
                Log.e("Fix Permission failed on %s", e, Build.FINGERPRINT);
                throw new SmartHostsException("Fix Permission failed on "+Build.FINGERPRINT, e);
            } catch (IOException ex) {
                Log.e("Create temp file failed on %s", e, Build.FINGERPRINT);
                throw new SmartHostsException("Create temp file failed on "+Build.FINGERPRINT, e);
            }
        }

        try {
            fos.write(bytes);
            fos.close();
        } catch (IOException e) {
            Log.e(e.getMessage(), e);
            throw new SmartHostsException(e);
        } finally {
            closeStream(fos);
        }

        return context.getFileStreamPath(TEMP_FILE_NAME);
    }

    public static File stringToTmpeFile(Context context, String string) {
        return bytesToTempFile(context, string.getBytes());
    }

    public static byte[] tempFileToBytes(Context context) {
        int len = 1024;
        byte[] buffer = new byte[len];
        FileInputStream fis = null;
        try {
            fis = context.openFileInput(TEMP_FILE_NAME);
        } catch (FileNotFoundException e) {
            Log.w("Permission error. Fix permissiong and retry...");
            ShellExecutor.executeRootCommand("chmod 666 "+getTempFile(context).getAbsolutePath());
            try {
                fis = context.openFileInput(TEMP_FILE_NAME);
            } catch (FileNotFoundException ex) {
                Log.e("Fix Permission failed on %s", e, Build.FINGERPRINT);
                throw new SmartHostsException("Fix Permission failed on "+Build.FINGERPRINT, e);
            }
        }

        try {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int nrb = fis.read(buffer, 0, len); // read up to len bytes
            while (nrb != -1) {
                baos.write(buffer, 0, nrb);
                nrb = fis.read(buffer, 0, len);
            }
            buffer = baos.toByteArray();
            fis.close();
        } catch (FileNotFoundException e) {
            Log.e(e.getMessage(), e);
            throw new SmartHostsException(e);
        } catch (IOException e) {
            Log.e(e.getMessage(), e);
            throw new SmartHostsException(e);
        }
        return buffer;
    }

    public static String tempFileToString(Context context) {
        byte[] bytes = tempFileToBytes(context);
        if (bytes == null) return null;
        return new String(bytes);
    }

    public static File getTempFile(Context context) {
        return context.getFileStreamPath(TEMP_FILE_NAME);
    }

    //******************************* Stream **********************************

    public static void closeStream(Closeable stream) {
        if (stream != null) {
            try {
                stream.close();
            } catch (IOException e) {
                Log.e("Could not close stream", e);
                throw new SmartHostsException(e);
            }
        }
    }

}
